/*
Copyright (C) 1996-1997 Id Software, Inc.

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 3
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

*/

// screen.c -- master for refresh, status bar, console, chat, notify, etc

#include "quakedef.h"

// global screen state
scrdef_t scr;

cvar_t *scr_viewsize;
cvar_t *scr_fov;
cvar_t *scr_conspeed;
cvar_t *scr_centertime;
cvar_t *scr_showram;
cvar_t *scr_showturtle;
cvar_t *scr_showpause;
cvar_t *scr_printspeed;
cvar_t *scr_showfps;

extern	cvar_t	*crosshair;

qpic_t		*scr_ram;
qpic_t		*scr_net;
qpic_t		*scr_turtle;

void SCR_ScreenShot_f (void);

void SCR_DisableForLoading (BOOL disable)
{
	if (disable && !scr.DisabledForLoading)
	{
		scr.DisabledForLoading = TRUE;
		scr.DisabledTime = realtime;
	}
	else if (!disable && scr.DisabledForLoading)
		scr.DisabledForLoading = FALSE;
}


/*
==============
SCR_CenterPrint

Called for important messages that should stay in the center of the screen
for a few moments
==============
*/
void SCR_CenterPrint (char *str)
{
	if (!str || !str[0])
	{
		// some progs send an empty string to clear the current string
		scr.CentertimeOff = -1;
		return;
	}

	strncpy (scr.CenterString, str, sizeof (scr.CenterString) - 1);
	scr.CentertimeOff = cl.time + scr_centertime->value;
	scr.CentertimeStart = cl.time;

	// count the number of lines for centering
	scr.CenterLines = 1;

	while (*str)
	{
		if (*str == '\n')
			scr.CenterLines++;

		str++;
	}
}


void SCR_DrawCenterString (void)
{
	scr.EraseCenter = 0;

	// the finale prints the characters one at a time
	int	remaining = (cl.intermission) ? scr_printspeed->value * (cl.time - scr.CentertimeStart) : 9999;

	char *start = scr.CenterString;
	int y = (vid.Height * 0.333) - (scr.CenterLines * 5);

	do
	{
		// re-init the buffer
		char destbuf[42] = {0};

		// scan the width of the line
		for (int j = 0; j < 40; j++)
		{
			if (start[j] == '\n' || !start[j]) break;

			// copy off so that we can safely trim it in-place (ensure that it's always null-terminated)
			destbuf[j] = start[j];
			destbuf[j + 1] = 0;
		}

		// trim the string
		char *trimmed = Q_strtrim (destbuf);

		// re-evaluate length
		int l = strlen (trimmed);

		// this is now the string length to draw so center and draw it
		int x = (vid.Width - l * 8) / 2;

		// and draw the individual characters
		for (int j = 0; j < l; j++, x += 8)
		{
			Draw_Character (x, y, trimmed[j]);

			if (!remaining--)
				return;
		}

		y += 10;

		// skip to the end of the line
		while (*start && *start != '\n')
			start++;

		if (!*start)
			break;

		start++;		// skip the \n
	} while (1);
}


void SCR_CheckDrawCenterString (void)
{
	if (scr.CenterLines > scr.EraseLines)
		scr.EraseLines = scr.CenterLines;

	if (scr.CentertimeOff > cl.time || cl.intermission)
	{
		if (key_dest != key_game)
		{
			// don't linger once we leave a game
			scr.CentertimeOff = -1;
			return;
		}

		SCR_DrawCenterString ();
	}
}


/*
====================
CalcFov
====================
*/
float SCR_CalcFovX (float fov_y, float width, float height)
{
	// bound, don't crash
	if (fov_y < 1) fov_y = 1;
	if (fov_y > 179) fov_y = 179;

	return (atan (width / (height / tan ((fov_y * M_PI) / 360.0f))) * 360.0f) / M_PI;
}


float SCR_CalcFovY (float fov_x, float width, float height)
{
	// bound, don't crash
	if (fov_x < 1) fov_x = 1;
	if (fov_x > 179) fov_x = 179;

	return (atan (height / (width / tan ((fov_x * M_PI) / 360.0f))) * 360.0f) / M_PI;
}


void SCR_SetFOV (refdef_t *rd, float fovvar, int width, int height)
{
	float aspect = (float) height / (float) width;

	// set up relative to a baseline aspect of 640x480 with a 48-high sbar
#define BASELINE_W	640.0f
#define BASELINE_H	432.0f

	// http://www.gamedev.net/topic/431111-perspective-math-calculating-horisontal-fov-from-vertical/
	// horizontalFov = atan (tan (verticalFov) * aspectratio)
	// verticalFov = atan (tan (horizontalFov) / aspectratio)
	if (aspect > (BASELINE_H / BASELINE_W))
	{
		// use the same calculation as GLQuake did (horizontal is constant, vertical varies)
		rd->FovX = fovvar;
		rd->FovY = SCR_CalcFovY (rd->FovX, width, height);
	}
	else
	{
		// alternate calculation (vertical is constant, horizontal varies)
		// consistent with http://www.emsai.net/projects/widescreen/fovcalc/
		// note that the gun always uses this calculation irrespective of the aspect)
		rd->FovY = SCR_CalcFovY (fovvar, BASELINE_W, BASELINE_H);
		rd->FovX = SCR_CalcFovX (rd->FovY, width, height);
	}
}


/*
=================
SCR_CalcRefdef

Must be called whenever vid changes
Internal use only
=================
*/
static void SCR_CalcRefdef (void)
{
	if (!vid.RecalcRefdef) return;

	// bound viewsize
	if (scr_viewsize->value < 100) Cvar_SetValue (scr_viewsize, 100);
	if (scr_viewsize->value > 120) Cvar_SetValue (scr_viewsize, 120);

	// bound field of view
	if (scr_fov->value < 10) Cvar_SetValue (scr_fov, 10);
	if (scr_fov->value > 170) Cvar_SetValue (scr_fov, 170);

	// intermission is always full screen
	if (cl.intermission || scr_viewsize->value >= 120)
		vid.SbarLines = 0;
	else if (scr_viewsize->value >= 110)
		vid.SbarLines = 24;
	else vid.SbarLines = 48;

	// set FOV appropriately
	SCR_SetFOV (&ref, scr_fov->value, vid.Width, vid.Height - vid.SbarLines);

	// and done
	vid.RecalcRefdef = FALSE;
}


/*
=================
SCR_SizeUp_f

Keybinding command
=================
*/
void SCR_SizeUp_f (void)
{
	if (scr_viewsize->value < 120)
	{
		Cvar_SetValue (scr_viewsize, scr_viewsize->value + 10);
		vid.RecalcRefdef = TRUE;
	}
}


/*
=================
SCR_SizeDown_f

Keybinding command
=================
*/
void SCR_SizeDown_f (void)
{
	if (scr_viewsize->value > 100)
	{
		Cvar_SetValue (scr_viewsize, scr_viewsize->value - 10);
		vid.RecalcRefdef = TRUE;
	}
}


void SCR_RecalcRefdef (void)
{
	// this exists so that we don't have to compare cvars vs an "old" value to determine if we need a recalc
	vid.RecalcRefdef = TRUE;
}


void SCR_InitPics (void)
{
	/*
	// create the palette row textures
	for (int i = 0; i < 16; i++)
	{
		// in case this came from a game change
		SAFE_RELEASE (PaletteRowTextures[i]);

		IDirect3DTexture9 *tex = D3D_CreateManagedTexture (16, 1, 1, D3DFMT_X8R8G8B8);
		D3DLOCKED_RECT lockrect;

		if (SUCCEEDED (tex->LockRect (0, &lockrect, NULL, 0)))
		{
			unsigned *data = (unsigned *) lockrect.pBits;

			for (int j = 0, k = 15; j < 16; j++, k--)
				data[j] = d_8to24table_rgba[i * 16 + ((i > 7 && i < 14) ? k : j)];

			tex->UnlockRect (0);
		}

		PaletteRowTextures[i] = tex;
	}

	// load the other stuff specific to a game
	scr_ram = Draw_PicFromWad ("ram");
	scr_net = Draw_PicFromWad ("net");
	scr_turtle = Draw_PicFromWad ("turtle");
	*/
}


/*
==================
SCR_Init
==================
*/
void SCR_Init (void)
{
	// load one-time-only stuff
	scr_viewsize = Cvar_Get ("viewsize", "100", CVAR_ARCHIVE, SCR_RecalcRefdef);
	scr_fov = Cvar_Get ("fov", "90", 0, SCR_RecalcRefdef);
	scr_conspeed = Cvar_Get ("scr_conspeed", "3000");
	scr_centertime = Cvar_Get ("scr_centertime", "2");
	scr_showram = Cvar_Get ("showram", "1");
	scr_showturtle = Cvar_Get ("showturtle", "0");
	scr_showpause = Cvar_Get ("showpause", "1");
	scr_printspeed = Cvar_Get ("scr_printspeed", "32");

#ifdef _DEBUG
	scr_showfps = Cvar_Get ("scr_showfps", "1", CVAR_ARCHIVE);
#else
	scr_showfps = Cvar_Get ("scr_showfps", "0", CVAR_ARCHIVE);
#endif

	// register our commands
	Cmd_AddCommand ("screenshot", SCR_ScreenShot_f);
	Cmd_AddCommand ("sizeup", SCR_SizeUp_f);
	Cmd_AddCommand ("sizedown", SCR_SizeDown_f);

	// init scr stuff
	scr.CentertimeOff = -1;
}


/*
==============
DrawPause
==============
*/
void SCR_DrawPause (void)
{
	qpic_t	*pic;

	// turn off for screenshots
	if (!scr_showpause->value) return;
	if (!cl.paused) return;

	pic = Draw_CachePic ("gfx/pause.lmp");
	Draw_Pic ((vid.Width - pic->width) / 2, (vid.Height - 48 - pic->height) / 2, pic);
}



/*
==============
SCR_DrawLoading
==============
*/
void SCR_DrawLoading (void)
{
	qpic_t	*pic;

	if (!scr.DrawLoading)
		return;

	pic = Draw_CachePic ("gfx/loading.lmp");
	Draw_Pic ((vid.Width - pic->width) / 2, (vid.Height - 48 - pic->height) / 2, pic);
}


/*
==================
SCR_SetUpToDrawConsole
==================
*/
void SCR_SetUpToDrawConsole (void)
{
	// make this independent of timescale
	static double con_lasttime = -1;
	double con_frametime = 0;

	if (con_lasttime < 0)
		con_lasttime = realtime;

	con_frametime = realtime - con_lasttime;
	con_lasttime = realtime;

	Con_CheckResize ();

	// never a console with loading plaque
	if (scr.DrawLoading) return;

	// decide on the height of the console
	con_forcedup = (!cl.worldmodel || cls.signon != SIGNONS);

	if (con_forcedup)
	{
		scr.ConLines = vid.Height;		// full screen
		scr.ConCurrent = scr.ConLines;
	}
	else if (key_dest == key_console)
		scr.ConLines = vid.Height / 2;	// half screen
	else
		scr.ConLines = 0;				// none visible

	if (scr.ConLines < scr.ConCurrent)
	{
		scr.ConCurrent -= scr_conspeed->value * con_frametime;

		if (scr.ConLines > scr.ConCurrent)
			scr.ConCurrent = scr.ConLines;
	}
	else if (scr.ConLines > scr.ConCurrent)
	{
		scr.ConCurrent += scr_conspeed->value * con_frametime;

		if (scr.ConLines < scr.ConCurrent)
			scr.ConCurrent = scr.ConLines;
	}
}


/*
==================
SCR_DrawConsole
==================
*/
void SCR_DrawConsole (void)
{
	if (scr.ConCurrent)
	{
		Con_DrawConsole (scr.ConCurrent, TRUE);
	}
	else
	{
		if (key_dest == key_game || key_dest == key_message)
			Con_DrawNotify ();	// only draw notify in game
	}
}


byte *SCR_GetScreenData (D3D10_TEXTURE2D_DESC *desc, int width, int height)
{
	byte *buffer = NULL;

	ID3D10Texture2D *scr_backbuffer = NULL;
	ID3D10Texture2D *scr_copy = NULL;

	if (SUCCEEDED (d3d_SwapChain->GetBuffer (0, __uuidof (ID3D10Texture2D), (LPVOID *) &scr_backbuffer)))
	{
		// copy off the desc to ensure that the new one matches
		scr_backbuffer->GetDesc (desc);

		// set the stuff we want to differ
		desc->Usage = D3D10_USAGE_STAGING;
		desc->BindFlags = 0;
		desc->CPUAccessFlags = D3D10_CPU_ACCESS_WRITE | D3D10_CPU_ACCESS_READ;

		if (SUCCEEDED (d3d_Device->CreateTexture2D (desc, NULL, &scr_copy)))
		{
			d3d_Device->CopySubresourceRegion (scr_copy, 0, 0, 0, 0, scr_backbuffer, 0, NULL);
			d3d_Device->Flush ();

			D3D10_MAPPED_TEXTURE2D MappedResource;

			if (SUCCEEDED (scr_copy->Map (0, D3D10_MAP_READ_WRITE, 0, &MappedResource)))
			{
				// discard the row pitch in-place
				D3D_CollapseRowPitch ((unsigned *) MappedResource.pData, desc->Width, desc->Height, MappedResource.RowPitch >> 2);

				if (desc->Width > width)
				{
					// collapse the pitch down again if we need to
					D3D_CollapseRowPitch ((unsigned *) MappedResource.pData, width, height, desc->Width);

					// copy them back
					desc->Width = width;
					desc->Height = height;
				}

				// and this is the buffer we'll use so compress into it
				buffer = (byte *) qScratch->FastAlloc (desc->Width * desc->Height * 3);
				D3D_Compress32To24WithSwapToBGR (buffer, (byte *) MappedResource.pData, desc->Width, desc->Height);

				scr_copy->Unmap (0);
			}
			else Con_Printf (PRINT_SAFE, "SCR_GetScreenData : failed to map scratch texture\n");

			scr_copy->Release ();
		}
		else Con_Printf (PRINT_SAFE, "SCR_GetScreenData : failed to create scratch texture\n");

		scr_backbuffer->Release ();
	}
	else Con_Printf (PRINT_SAFE, "SCR_GetScreenData : failed to get backbuffer\n");

	return buffer;
}


void SCR_MapShot (char *name)
{
	// refresh the screen to get a new back buffer
	scr.DrawMapshot = TRUE;
	SCR_UpdateScreen (0);
	scr.DrawMapshot = FALSE;

	byte *buffer = NULL;
	int hunkmark = qScratch->GetLowMark ();
	D3D10_TEXTURE2D_DESC desc;

	// didn't get it for some reason
	if (!(buffer = SCR_GetScreenData (&desc, MAPSHOT_SIZE, MAPSHOT_SIZE)))
	{
		qScratch->FreeToLowMark (hunkmark);
		Con_Printf (PRINT_SAFE, "SCR_Screenshot_f : failed\n");
		return;
	}

	D3D_WriteDataToTGA (name, buffer, desc.Width, desc.Height, 24);
	qScratch->FreeToLowMark (hunkmark);

	// invalidate our current mapshot
	Draw_InvalidateMapshot ();
}


/*
==================
SCR_ScreenShot_f
==================
*/
void SCR_ScreenShot_f (void)
{
	int i;
	char checkname[MAX_OSPATH];

	// create the directory for screenshots
	FS_CreateDirectory ("screenshot");

	// find a file name to save it to
	for (i = 0; i <= 99; i++)
	{
		// make the name
		sprintf (checkname, "%s/screenshot/quake%02i.tga", com_gamedir, i);

		if (Sys_FileTime (checkname) == -1)
			break;	// file doesn't exist
	}

	if (i == 100)
	{
		Con_Printf (PRINT_DEFAULT, "SCR_ScreenShot_f: Couldn't create a TGA file\n");
		return;
	}

	byte *buffer = NULL;
	int hunkmark = qScratch->GetLowMark ();
	D3D10_TEXTURE2D_DESC desc;

	// didn't get it for some reason
	if (!(buffer = SCR_GetScreenData (&desc, vid.Width, vid.Height)))
	{
		qScratch->FreeToLowMark (hunkmark);
		Con_Printf (PRINT_SAFE, "SCR_Screenshot_f : failed\n");
		return;
	}

	D3D_WriteDataToTGA (checkname, buffer, desc.Width, desc.Height, 24);
	qScratch->FreeToLowMark (hunkmark);

	// report
	Con_Printf (PRINT_SAFE, "Wrote %s\n", checkname);
}


/*
===============
SCR_BeginLoadingPlaque

================
*/
void SCR_BeginLoadingPlaque (void)
{
	S_StopAllSounds (TRUE);

	if (cls.state != ca_connected) return;
	if (cls.signon != SIGNONS) return;

	// redraw with no console and the loading plaque
	Con_ClearNotify ();
	scr.CentertimeOff = -1;
	scr.ConCurrent = 0;

	scr.DrawLoading = TRUE;
	SCR_UpdateScreen (0);
	SCR_DisableForLoading (TRUE);
	scr.DrawLoading = FALSE;
}

/*
===============
SCR_EndLoadingPlaque

================
*/
void SCR_EndLoadingPlaque (void)
{
	SCR_DisableForLoading (FALSE);
	Con_ClearNotify ();
}


void SCR_DrawNotifyString (void)
{
	int lines = 0;
	int width = 0;
	int linewidth = 0;

	for (int i = 0; ; i++)
	{
		if (!scr.NotifyString[i]) break;

		if (scr.NotifyString[i] == '\n')
		{
			if (linewidth > width) width = linewidth;

			linewidth = 0;
			lines++;
		}

		linewidth++;
	}

	char *start = scr.NotifyString;
	int y = vid.Height * 0.35 - lines * 5;

	// put a text box around it
	width = (width + 2) & ~1;
	lines = ((lines + 1) * 10) / 8;
	Draw_TextBox ((vid.Width - width * 8) / 2 - 8, y - 10, width, lines);

	do
	{
		// scan the width of the line
		for (int l = 0; ; l++)
		{
			if (start[l] == '\n' || !start[l])
			{
				int x = (vid.Width - l * 8) / 2;

				for (int j = 0; j < l; j++, x += 8)
					Draw_Character (x, y, start[j]);

				break;
			}
		}

		y += 10;

		while (*start && *start != '\n')
			start++;

		if (!*start)
			break;

		start++;		// skip the \n
	} while (1);
}

/*
==================
SCR_ModalMessage

Displays a text string in the center of the screen and waits for a Y or N
keypress.
==================
*/
int SCR_ModalMessage (char *text)
{
	// draw a fresh screen
	scr.DrawDialog = TRUE;
	scr.NotifyString = text;

	// so dma doesn't loop current sound
	S_ClearBuffer ();

	do
	{
		// rearm the timers and run a screen update as this may take some time
		Host_RearmTimers ();
		SCR_UpdateScreen (0);

		// don't consume CPU unnecessarily while waiting
		MsgWaitForMultipleObjects (0, NULL, FALSE, 10, QS_ALLINPUT);

		// wait for a key down and up
		key_count = -1;
		Sys_SendKeyEvents (TRUE);
	} while (key_lastpress != 'y' && key_lastpress != 'n' && key_lastpress != K_ESCAPE);

	// redraw the screen to remove the dialog
	scr.DrawDialog = FALSE;
	scr.NotifyString = NULL;
	Host_RearmTimers ();
	SCR_UpdateScreen (0);

	return key_lastpress == 'y';
}


void SCR_ShowFPS (double r_speeds_ms)
{
	static int frames = 0;
	static double starttime = 0;
	static BOOL first = TRUE;
	static float fps = 0.0f;

	if (first || cls.state != ca_connected)
	{
		frames = 0;
		starttime = realtime;
		first = FALSE;
		return;
	}

	// always keep it up to date even if scr_showfps is 0 so that when scr_showfps is set to 1 it will be immediately valid
	frames++;

	if (realtime - starttime > 0.25 && frames > 10)
	{
		fps = (double) frames / (realtime - starttime);
		starttime = realtime;
		frames = 0;
	}

	if (!con_forcedup && r_speeds->value)
	{
		extern int r_numactivelights;

		Draw_String (vid.Width - 120, 4, va ("%8.2f fps", fps));
		Draw_String (vid.Width - 120, 18, va ("%8.2f ms", (float) r_speeds_ms));
		Draw_String (vid.Width - 120, 28, va ("%8i wpoly", c_brush_polys));
		Draw_String (vid.Width - 120, 38, va ("%8i epoly", c_alias_polys));
		Draw_String (vid.Width - 120, 48, va ("%8i draw", c_draw_call));
		Draw_String (vid.Width - 120, 58, va ("%8i light", r_numactivelights));
	}
	else if (scr_showfps->value)
		Draw_String (vid.Width - 104, 4, va ("%8.2f fps", fps));
}


/*
==================
SCR_UpdateScreen

This is called every frame, and can also be called explicitly to flush
text to the screen.

WARNING: be very careful calling this from elsewhere, because the refresh
needs almost the entire 256k of stack space!
==================
*/
void SCR_UpdateScreen (double frametime)
{
	double r_speeds_begin;
	double r_speeds_ms;
	static BOOL recursive = FALSE;

	// wipe the temp pool here too in case anything calls SCR_UpdateScreen multiple times inside the main loop
	int mark = qScratch->GetLowMark ();

	// prevent from being called recursively
	if (recursive) return;

	// check for disabling drawing
	if (!vid.ActiveApp || vid.Minimized) return;

	if (scr.DisabledForLoading)
	{
		if (realtime - scr.DisabledTime > 666)
		{
			SCR_DisableForLoading (FALSE);
			Con_Printf (PRINT_SAFE, "load failed.\n");
		}
		else return;
	}

	// not initialized yet
	if (!scr.Initialized || !con_initialized) return;

	// prevent from being called recursively
	recursive = TRUE;

	if (GL_BeginRendering ())
	{
		// determine size of refresh window
		SCR_CalcRefdef ();

		// do 3D refresh drawing, and then update the screen
		SCR_SetUpToDrawConsole ();

		if (!con_forcedup)
		{
			if (r_speeds->value)
			{
				r_speeds_begin = Sys_DoubleTime ();
				c_brush_polys = 0;
				c_alias_polys = 0;
				c_draw_call = 0;

				V_RenderView (frametime);

				r_speeds_ms = (Sys_DoubleTime () - r_speeds_begin) * 1000.0;
			}
			else
			{
				V_RenderView (frametime);
				r_speeds_ms = 0;
			}
		}

		GL_Set2D ();

		if (scr.DrawMapshot)
		{
			// skip all 2D overlay
		}
		else if (scr.DrawLoading)
		{
			SCR_DrawLoading ();
			Sbar_Draw ();
			SCR_ShowFPS (r_speeds_ms);
		}
		else if (cl.intermission == 1 && key_dest == key_game)
		{
			Draw_FadeScreen ();
			SCR_ShowFPS (r_speeds_ms);
			Sbar_IntermissionOverlay ();
		}
		else if (cl.intermission == 2 && key_dest == key_game)
		{
			Draw_FadeScreen ();
			SCR_ShowFPS (r_speeds_ms);
			Sbar_FinaleOverlay ();
			SCR_CheckDrawCenterString ();
		}
		else
		{
			SCR_DrawPause ();
			SCR_CheckDrawCenterString ();
			Sbar_Draw ();
			SCR_ShowFPS (r_speeds_ms);
			SCR_DrawConsole ();
			M_Draw ();
		}

		// draw the notify string last so that it retains the previous screen
		if (scr.DrawDialog)
		{
			// fade off the screen again so that the notify string will stand out, then draw it
			Draw_FadeScreen ();
			SCR_DrawNotifyString ();
		}

		GL_End2D ();
		CL_DecayLights ();
		GL_EndRendering ();
	}

	// wipe the temp pool here too in case anything calls SCR_UpdateScreen multiple times inside the main loop
	qScratch->FreeToLowMark (mark);

	// prevent from being called recursively
	recursive = FALSE;
}

